# Python 趣味编程：第 9 章　
# 拓展练习 9.6 结束判定和3×3的完成
# --------------------------
# 程序名: ex09-mine.py

from tkinter import Tk, Canvas, CENTER, NW
from dataclasses import dataclass, field
import math
import random
import sys

# =================================================
# 初始设定值
OFFSET_X = 100
OFFSET_Y = 100
CELL_SIZE = 40
FONT_SIZE = 20
CELL_CENTER = CELL_SIZE/2

BOARD_WIDTH = 3
BOARD_HEIGHT = 3
NUM_MINES = 2

FONT = "Helvetica " + str(FONT_SIZE)

# -------------------
# 管理整个游戏的棋局
@dataclass
class Board:
    width: int
    height: int
    mine: list = field(init=False)
    is_open: list = field(init=False)
    def __post_init__(self):
        self.mine = self.false_table()     # 地雷位置:初始化列表
        self.is_open = self.false_table()  # 打开的方格:初始化列表
        self.unopened = self.height * self.width    # 还没有打开的方格数
        self.exploded = False             # 爆炸了!

    # 创建只有False的二维列表
    def false_table(self):
        cells = [[False for y in range(self.height)]
                        for x in range(self.width)]
        return cells

    # 做游戏的准备
    def setup(self, num_mines):
        canvas.bind('<Button-1>', self.on_click)  # 注册活动程序
        self.set_mine(num_mines)
        self.unopened -= num_mines
        self.draw()                     # 显示第一个画面

    # 设置地雷
    def set_mine(self, num):
        num_mine = 0
        while num_mine < num:  # 按指定数量制造地雷
            i = random.randrange(self.width)   # 横方向
            j = random.randrange(self.height)  # 纵方向
            if not self.mine[i][j]:  # 如果地雷还没有被设置的话
                self.mine[i][j] = True  # 这里设置的地雷
                num_mine += 1  # 将已经生成的地雷数增加1
                print("mine = ({}, {})".format(i, j)) # 开发时的确认用

    # 方格索引是否有效的判断
    def is_valid(self, i, j):
        return 0 <= i < self.width and 0 <= j < self.height

    def open(self, i, j):
        self.is_open[i][j] = True   # 将“开放”设置为True
        if self.mine[i][j]:
            self.exploded = True    # 踩到地雷了!
        else:
            self.unopened -= 1

    # 打开方格:只被on_click呼叫
    # 用on_click检查方格索引的有效性
    def try_open(self, i, j):
        if self.is_valid(i, j):  # 有效的索引
            self.open(i, j)      # 打开方格
            self.draw()          # 重新绘制

    # 创建表示周围方格的元组列表
    def neighbors(self, i, j):
        x = [(i-1, j-1), (i, j-1), (i+1, j-1),
             (i-1, j  ),           (i+1, j  ),
             (i-1, j+1), (i, j+1), (i+1, j+1)]
        value = [v for v in x if self.is_valid(v[0], v[1])]
        return value

    # 数周围地雷的数量
    # neighbours中，is_valid已确认
    def count(self, i, j):
        c = 0
        for x in self.neighbors(i, j):  # 取出元组
            if self.mine[x[0]][x[1]]: # 如果有效方格中地雷的位置是True
                c = c + 1                # 向上计数 
        return c

    # ===== Controller =====
    # 点击时的处理
    def on_click(self, event):
        x, y = (event.x, event.y)
        i = math.floor((x - OFFSET_X) / CELL_SIZE) # 从x计算索引i
        j = math.floor((y - OFFSET_Y) / CELL_SIZE) # 从y计算索引i
        print(i, j)  # 在调试时确认计算正确的方法的例子
        self.try_open(i, j)

    def play(self):
        self.setup(NUM_MINES)   # 设定游戏
        while self.unopened > 0 and not self.exploded:
            tk.update()
        if self.exploded:       # 踩到地雷就结束了
            str = "Bang !"
        else:                   # 全部打开结束
            str = "Cleared !"
        canvas.create_text(OFFSET_X, OFFSET_Y + CELL_SIZE * self.height + 2,
			   text=str, font=FONT, anchor=NW)
        canvas.unbind('<Button-1>')  # 解放活动处理程序

    # ===== View =====
    # 画板的绘制
    def draw(self):
        canvas.delete("all")                # 暂时清除
        for i in range(self.width):         # i是宽度方向的下标
            for j in range(self.height):    # j是高度方向的下标
                text = ""
                if self.is_open[i][j]:   # 方格是开着的
                    if self.mine[i][j]:  # 如果是地雷
                        text = "*"            # "*"
                    else:                     # 如果不是地雷的话
                        text = str(self.count(i, j))  # 计数数字表示
                else:                         # 如果方格没有打开
                    text = "-"                # 显示"-"
                self.draw_text(i, j, text)     # 文本的显示

    # 方格的显示(框和文字)
    def draw_text(self, i, j, text):
        x = OFFSET_X + i * CELL_SIZE      # 从索引i计算×坐标
        y = OFFSET_Y + j * CELL_SIZE      # 从索引j计算y坐标
        canvas.create_rectangle(x, y, x + CELL_SIZE, y + CELL_SIZE) # 框架
        canvas.create_text(x + CELL_CENTER, y + CELL_CENTER,
			   text=text, font=FONT, anchor=CENTER)

# -------------------
tk = Tk()
tk.title("Mine Sweeper")

canvas = Canvas(tk, width=500, height=400, bd=0)
canvas.pack()

# ----------------------------------
# 主程序
board = Board(BOARD_WIDTH, BOARD_HEIGHT)
board.play()
sys.exit()
